小明的公司提供一套肯特機系統, 客戶希望系統能夠提供消費上限機制, 客戶希望系統能夠提供至少三種設定, 設定規則如下:
客戶設定儲存之後,
於是小明按照需求設計了一個儲存設定的物件
public class ChargeLimitConfig
{
public ChargeLimitConfig(int limit1Day, int limit7Day, int limit30Day, DateTime lastModified)
{
Day1Limit = new DepositLimit(limit1Day);
Day7Limit = new DepositLimit(limit7Day);
Day30Limit = new DepositLimit(limit30Day);
LastModified = lastModified;
}
public DateTime LastModified { get; }
public DepositLimit Day1Limit { get; }
public DepositLimit Day7Limit { get; }
public DepositLimit Day30Limit { get; }
}
然後針對"消費金額" 做了一個物件,
public class ChargeLimit
{
public ChargeLimit(int amount)
{
Amount = amount;
}
private bool IsUnlimited => Amount == 0;
private bool Islimited => Amount > 0;
private int Amount { get; }
public bool IsMoreThan(ChargeLimit other)
{
return IsIncreaseMoreFrom(other) || ChangeFromLimitedToUnlimited(other);
}
private bool ChangeFromLimitedToUnlimited(ChargeLimit other)
{
return (IsUnlimited && other.Islimited);
}
private bool IsIncreaseMoreFrom(ChargeLimit other)
{
return Islimited && other.Islimited && Amount > other.Amount;
}
}
特別注意的地方是,
裡面程式碼將上限金額為0 時, 表示為無上限
接著小明寫了一個檢查設定儲存驗證物件, 企圖用Validate 方法來驗證是否允許客戶修改設定內容
public class ChargeLimitUpdateValidator
{
private readonly ChargeLimitConfig _newConfig;
private readonly ChargeLimitConfig _oldConfig;
public ChargeLimitUpdateValidator(ChargeLimitConfig newConfig, ChargeLimitConfig oldConfig)
{
_newConfig = newConfig;
_oldConfig = oldConfig;
}
public bool Validate()
{
if ((IncreaseDay1LimitAmount() || IncreaseDay7LimitAmount() || IncreaseDay30LimitAmount())
&& ModifiedWithinRestrictionTimespan()) return false;
return true;
}
private bool IncreaseDay1LimitAmount()
{
return _newConfig.Day1Limit.IsMoreThan(_oldConfig.Day1Limit);
}
private bool IncreaseDay7LimitAmount()
{
return _newConfig.Day7Limit.IsMoreThan(_oldConfig.Day7Limit);
}
private bool IncreaseDay30LimitAmount()
{
return _newConfig.Day30Limit.IsMoreThan(_oldConfig.Day30Limit);
}
private bool ModifiedWithinRestrictionTimespan()
{
return _newConfig.LastModified - _oldConfig.LastModified <
new TimeSpan(24, 0, 0);
}
}
在系統中, 小明就用下面程式碼來檢查是否可以讓客戶更動設定
var updateValidator = new ChargeUpdateValidator(newConfig, oldConfig);
if( updateValidator.Validate() ){
//Save newConfig to Database
}
觀察上述的程式碼, 可以發現到
商業邏輯規則(客戶設定修改儲存限制), 散落在ChargeLimit 物件和ChargeLimitUpdateValidator 物件. 如果要新增規則, 就很難維護修改.
有重複的程式碼
IncreaseDay1LimitAmount()
IncreaseDay7LimitAmount()
IncreaseDay30LimitAmount()
設計守則
找出程式碼可能更動的地方, 把它們獨立出來,
不要和不太改動的地方放在一起.
從上述的需求可以看出可能更動的地方
如果你有大量的資料型別的資料, 就考慮可能將資料組織起來,形成一個物件類.
故看到這些1 天,7 天,30 天這些設定的資料, 就該考慮把這些設定資料集合起來變成物件類.
集合資料的方法有很多種, 資料結構可以用陣列也可以利用集合.
在這案例, 可以考慮用Dictionary
public class ChargeLimit
{
public int PeriodDays { get; set; }
public int Amount { get; set; }
}
public class ChargeLimitsConfig
{
public Dictionary<int, ChargeLimit> PeriodDayLimits { get; set; }
public DateTime LastModifiedTime { get; set; }
}
接下來看ChargeLimitUpdateValidator::Validate() 這個函數, 這個函數需要用數條規則來驗證使用者的參數內容(ChargeLimitsConfig)是否允許被修改
public class ChargeLimitUpdateValidator
{
public bool Validate(ChargeLimitsConfig oldConfig, ChargeLimitsConfig newConfig)
{
//這裡需要實作數條規則來驗證
}
}
設計守則 當看到數條規則的時候, 我們應該試著考慮設計一個驗證模式(Validation Pattern)
設計驗證應該滿足下面條件
所以我們要建立驗證介面, 這個驗證方法需要"舊的ChargeLimit", "修改時間", "新的ChargeLimit", 以及"新的修改時間"四個參數
public interface IChargeLimitUpdateRule
{
bool Validate(ChargeLimit oldLimit, DateTime lastModifyiedTime, ChargeLimit newLimit, DateTime modifyTime);
}
設計守則 函數(function)的參數應該盡可能地很少, 3個或更多參數對於一個函數來說太多了
所以我們要建立物件來包裝這四個參數
public class ValidateChargeLimitArgs
{
public ChargeLimit OldLimit { get; set; }
public DateTime LastModifiedTime { get; set; }
public ChargeLimit NewLimit { get; set; }
public DateTime ModifyTime { get; set; }
}
經過上面的重構, IChargeLimitUpdateRule 宣告如下
public interface IChargeLimitUpdateRule
{
void Handle(ValidateChargeLimitArgs args);
}
設計守則 規則實作 - 只要發現不符合規則, 我們就直接丟例外就好
首先設計規則1是 -- 不允許金額增加
public class CannotIncreaseRule : IChargeLimitUpdateRule
{
public void Handle(ValidateChargeLimitArgs args)
{
if (!args.OldLimit.IsUnlimit && !args.NewLimit.IsUnlimit)
{
if (args.OldChargeLimitSetting.Amount < args.NewChargeLimitSetting.Amount)
{
throw new ValidationException();
}
}
}
}
接著設計規則2, 不允許有上限金額改成無上限金額
public class CannotLimitToUnlimitRule : IChargeLimitUpdateRule
{
public void Handle(ValidateChargeLimitArgs args)
{
if (args.OldLimit.Islimit && args.NewLimit.IsUnlimit )
{
throw new ValidationException();
}
}
}
接著設計規則3, 24小時內要套用規則1 和規則2 , 這時候...
設計守則 多用組合, 少用繼承.
原因如下
故我們設計如下
public class In24HrRule : IChargeLimitUpdateRule
{
public void Handle(ValidateChargeLimitArgs args)
{
if (!TimeHelper.IsIn24Hr(args.LastModifiedTime, args.ModifyTime))
{
return;
}
var rule1 = new CannotIncreaseRule();
rule1.Handle(args);
var rule2 = new CannotLimitToUnlimitRule();
rule2.Handle(args);
}
}
回到ChargeLimitUpdateValidator::Validate() 的地方, 開始建立規則驗證實例並呼叫
public class ChargeLimitUpdateValidator
{
public bool Validate(ChargeLimitsConfig oldConfig, ChargeLimitsConfig newConfig)
{
var rule = new In24HrRule();
rule.Handle(xxx);
}
}
我們需要取得所有的使用者新舊設定資料, 故我們實作方法來做這件事情
private static IEnumerable<ValidateChargeLimitArgs> GetAllChargeLimits(ChargeLimitsConfig oldConfig, ChargeLimitsConfig newConfig)
{
var q1 = from tb1 in oldConfig.PeriodDayLimits.Values
join tb2 in newConfig.PeriodDayLimits.Values on tb1.PeriodDays equals tb2.PeriodDays
select new ValidateChargeLimitArgs()
{
OldLimit = tb1,
LastModifiedTime = oldConfig.LastModifiedTime,
NewLimit = tb2,
ModifyTime = DateTime.Now
};
return q1;
}
取得所有使用者新舊設定資料之後, 並且一個一個餵給規則去檢查,
然後用try...catch 方式檢查規則是否有丟出驗證例外, 如果有例外錯誤, 就回傳false
public bool Validate(ChargeLimitsConfig oldConfig, ChargeLimitsConfig newConfig)
{
var allChargeLimits = GetAllChargeLimits(oldConfig, newConfig);
var rule = new In24HrRule();
try
{
foreach (var item in allChargeLimits)
{
rule.Handle(item);
}
return true;
}
catch(ValidationException)
{
return false;
}
}
為了程式碼更乾淨一點, 我們可以把try...catch 抽取出去變成一個方法
private static bool HandleAllChargeLimitsByRule(IEnumerable<ValidateChargeLimitArgs> chargeLimits, IChainOfResponsibilityHandler<ValidateChargeLimitArgs> rule)
{
try
{
foreach (var limit in chargeLimits)
{
rule.Handle(limit);
}
return true;
}
catch
{
return false;
}
}
最後我們的驗證器方法變成如下
public bool Validate(ChargeLimitsConfig oldConfig, ChargeLimitsConfig newConfig)
{
var allChargeLimits = GetAllChargeLimits(oldConfig, newConfig);
var rule = new In24HrRule();
return HandleAllChargeLimitsByRule(allChargeLimits, rule);
}
這樣一來程式碼不就看起來清晰漂亮了嗎?
接下來如果想要加更多的規則, 我們更可以用責任鏈模式(Chain Of Responsibility Pattern)來設計規則.
責任鏈模式的特色 - 當這個物件沒有要處理或是處理完的時候, 能夠將這個請求(request) 傳遞給下一個物件繼續處理.
責任鏈物件的建構元(constructor) 通常都會有一個參數(下一個處理物件是誰)
public class MyHandler : IChainOfResponsibilityHandler
{
IChainOfResponsibilityHandler _nextHandler;
public MyHandler(IChainOfResponsibilityHandler nextHandler)
{
//儲存下一個處理物件
_nextHandler = nextHandler;
}
public void Handle(object request)
{
//處理完我的事情之後
...
//將這個請求(request) 傳遞給下一個處理物件繼續處理
_nextHandler?.Handle(request);
}
}
所以CannotIncreaseRule 規則的程式碼就會修改為如下
public class CannotIncreaseRule : IChargeLimitUpdateRule
{
IChargeLimitUpdateRule _nextHandler;
public CannotIncreaseRule(IChargeLimitUpdateRule nextHandler)
{
_nextHandler = nextHandler;
}
public void Handle(ValidateChargeLimitArgs args)
{
if (!args.OldLimit.IsUnlimit && !args.NewLimit.IsUnlimit)
{
if (args.OldChargeLimitSetting.Amount < args.NewChargeLimitSetting.Amount)
{
throw new ValidationException();
}
}
_nextHandler?.Handle(args);
}
}
另一條規則也修改如下
public class CannotLimitToUnlimitRule : IChargeLimitUpdateRule
{
IChargeLimitUpdateRule _nextHandler;
public CannotLimitToUnlimitRule(IChargeLimitUpdateRule nextHandler)
{
_nextHandler = nextHandler;
}
public void Handle(ValidateChargeLimitArgs args)
{
if (args.OldLimit.Islimit && args.NewLimit.IsUnlimit )
{
throw new ValidationException();
}
_nextHandler?.Handle(args);
}
}
然後在程式中使用這2 個規則的時候, 就可以這樣使用
var rules = new CannotIncreaseRule(new CannotLimitToUnlimitRule());
rules.Handle(request);
到這裡, 你會發現....
設計守則 不要有重複的程式碼
剛剛兩個規則中你可以發現有重複的程式碼(duplicate code).
設計守則 應當減少巢狀式的寫法
另外你發現初始化那一串物件的地方, 也是一種壞味道(巢狀式的寫法). 我們也能夠用其他方式來封裝.
所以我們把重複的程式碼抽出來變成另一個類. 然後新增一個方法SetNext 來指定下一個處理物件.
public abstract class BaseRule : IChargeLimitUpdateRule
{
IChargeLimitUpdateRule _nextHandler;
public IChargeLimitUpdateRule SetNext(IChargeLimitUpdateRule handler)
{
this._nextHandler = handler;
return this._nextHandler;
}
public virtual void Handle(ValidateChargeLimitArgs args) {
_nextHandler?.Handle(args);
}
}
然後把剛剛兩個規則改寫為如下
public class CannotIncreaseRule : BaseRule, IChargeLimitUpdateRule
{
public override void Handle(ValidateChargeLimitArgs args)
{
if (args.OldChargeLimitSetting.Amount < args.NewChargeLimitSetting.Amount)
{
throw new ValidationException();
}
base.Handle(args);
}
}
另外一個規則也改寫如下
public class CannotLimitToUnlimitRule : BaseRule, IChargeLimitUpdateRule
{
public override void Handle(ValidateChargeLimitArgs args)
{
if (args.OldLimit.Islimit && args.NewLimit.IsUnlimit )
{
throw new ValidationException();
}
base.Handle(args);
}
}
接著寫一個初始化一串Chain 的輔助方法
public static class ChainOfResponsibility {
public static IChargeLimitUpdateRule Chain(params IChargeLimitUpdateRule[] handlers)
{
var first = handlers.First();
var chain = first;
foreach (var handler in handlers.Skip(1))
{
chain = chain.SetNext(handler);
}
return first;
}
}
同樣地在程式中初始化Chain 這2 個規則以上的時候, 就可以這樣使用
var rules = ChainOfResponsibility.Chain(
new CannotIncreaseRule(),
new CannotLimitToUnlimitRule(),
new xxxRule1(),
new xxxRule2()
);
rules.Handle(request);
這樣一來就可以打破巢狀式的初始化寫法